﻿namespace Jsl.FxCop
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using Microsoft.FxCop.Sdk;

    /// <summary>
    /// Searches for overrides of the Dispose method and verifies they dispose of the components field correctly.
    /// </summary>
    public class DisposeComponentsFieldCorrectly : BaseIntrospectionRule
    {
        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="DisposeComponentsFieldCorrectly"/> class.
        /// </summary>
        public DisposeComponentsFieldCorrectly()
            : base(typeof(DisposeComponentsFieldCorrectly).Name, "Jsl.FxCop.Rules", typeof(DisposeComponentsFieldCorrectly).Assembly)
        {
        }
        #endregion Constructors

        #region Public Methods
        /// <summary>
        /// Checks the specified member.
        /// </summary>
        /// <param name="member">The member to check.</param>
        /// <returns>The problems found if any.</returns>
        public override ProblemCollection Check(Member member)
        {
            Method method = member as Method;
            if (!DisposeComponentsFieldCorrectly.IsDisposeMethod(method)
                || !DisposeComponentsFieldCorrectly.HasComponentsField(method))
            {
                return this.Problems;
            }

            List<MethodCall> disposeComponentsCalls = new List<MethodCall>();
            new CciVisitor()
                .AddVisitor<MethodCall>(call =>
                    {
                        if (DisposeComponentsFieldCorrectly.IsComponentsDispose(call))
                        {
                            disposeComponentsCalls.Add(call);
                        }
                    })
                .Visit(method);

            switch (disposeComponentsCalls.Count)
            {
                case 0:
                    this.Problems.Add(
                        new Problem(this.GetNamedResolution("MustDisposeComponents"), method.SourceContext));
                    break;

                case 1:
                    this.CheckDisposeCall(method, disposeComponentsCalls[0]);
                    break;

                default:
                    this.Problems.Add(
                        new Problem(this.GetNamedResolution("OnlyOneDispose"), method.SourceContext));
                    break;
            }

            return this.Problems;
        }
        #endregion Public Methods

        #region Private Methods
        private static bool HasComponentsField(Method method)
        {
            Field componentsField = method.DeclaringType.GetField(Identifier.For("components"));
            return (componentsField != null)
                && object.ReferenceEquals(componentsField.DeclaringType, method.DeclaringType);
        }

        private static bool IsComponentsDispose(MethodCall call)
        {
            Member calledMethod = call.GetMethod();
            if ((calledMethod == null)
                || (calledMethod.FullName != "System.IDisposable.Dispose"))
            {
                return false;
            }

            MemberBinding componentsBinding = call.GetTargetObject() as MemberBinding;
            if ((componentsBinding == null)
                || !(componentsBinding.TargetObject is This))
            {
                return false;
            }

            Field componentsField = componentsBinding.BoundMember as Field;
            if ((componentsField == null)
                || (componentsField.Name.Name != "components"))
            {
                return false;
            }

            return true;
        }

        private static bool IsDisposeMethod(Method method)
        {
            return (method != null)
                && (method.Name.Name == "Dispose")
                && (method.Parameters.Count == 1)
                && (method.Parameters[0].Type.FullName == "System.Boolean")
                && method.IsVirtual
                && (method.OverriddenMember != null);
        }

        private bool CallsDisposeBlock(Block block, Block disposeBlock)
        {
            return this.CallsDisposeBlockInDebugMode(block, disposeBlock)
                || this.CallsDisposeBlockInReleaseMode(block, disposeBlock);
        }

        private bool CallsDisposeBlockInDebugMode(Block block, Block disposeBlock)
        {
            // Looking for a boolean local assignment statement followed by a branch using the local.
            if (block.Statements.Count >= 2)
            {
                AssignmentStatement assignmentStatement = block.Statements[block.Statements.Count - 2] as AssignmentStatement;
                Branch branchStatement = block.Statements[block.Statements.Count - 1] as Branch;
                if ((assignmentStatement != null)
                    && (branchStatement != null)
                    && (branchStatement.Condition is Local))
                {
                    Local nullTestResult = assignmentStatement.Target as Local;
                    if (nullTestResult != null)
                    {
                        BinaryExpression condition = assignmentStatement.Source as BinaryExpression;
                        if ((condition != null) && (condition.NodeType == NodeType.Ceq))
                        {
                            Literal nullLiteral = condition.Operand2 as Literal;
                            if ((nullLiteral != null) && (nullLiteral.Value == null))
                            {
                                Field componentsField = (condition.Operand1 as MemberBinding).GetInstanceField();
                                if ((componentsField != null)
                                    && (componentsField.Name.Name == "components"))
                                {
                                    return true;
                                }
                            }
                        }
                    }
                }
            }

            return false;
        }

        private bool CallsDisposeBlockInReleaseMode(Block block, Block disposeBlock)
        {
            // Looking for the conditional branch with the (this.components != null) check.
            Branch branchStatement = block.Statements[block.Statements.Count - 1] as Branch;
            if (branchStatement != null)
            {
                UnaryExpression nullCheck = branchStatement.Condition as UnaryExpression;
                if (nullCheck != null)
                {
                    Field componentsField = (nullCheck.Operand as MemberBinding).GetInstanceField();
                    if ((componentsField != null)
                        && (componentsField.Name.Name == "components"))
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        private void CheckDisposeBlock(Method method, Block disposeBlock)
        {
            Block previousBlock = disposeBlock.GetPreviousBlock(method);
            if ((previousBlock == null)
                || !this.CallsDisposeBlock(previousBlock, disposeBlock))
            {
                this.Problems.Add(
                    new Problem(
                        this.GetNamedResolution("MustDisposeComponents"), disposeBlock.SourceContext));
            }
        }

        private void CheckDisposeCall(Method method, MethodCall disposeComponentsCall)
        {
            // Find the block that contains the dispose call.
            Block containingBlock = disposeComponentsCall.GetContainingBlock(method);
            if (containingBlock == null)
            {
                throw new InvalidOperationException("Couldn't find the containing block or statement for the dispose call.");
            }

            int otherStatements = containingBlock.Statements
                .Where(statement => statement.NodeType != NodeType.Nop)     // Ignore Nop's.
                .Count();

            if (otherStatements == 0)
            {
                throw new InvalidOperationException("Didn't find dispose statement in its containing block.");
            }

            if (otherStatements > 1)
            {
                this.Problems.Add(
                    new Problem(this.GetNamedResolution("DisposeShouldBeOnlyStatement"), disposeComponentsCall.SourceContext));
            }
            else
            {
                this.CheckDisposeBlock(method, containingBlock);
            }
        }
        #endregion Private Methods
    }
}